#include "mmflat.h"
#include "assert.h"
#include "general.h"

/* the end address of mmflat (region) */
#define END_ADDR(pmmflat) ((mx_void_pt)((mx_usize_t)pmmflat->start + pmmflat->size))
/* the head size in element */
#define SIZEOF_HEAD (sizeof(mx_mmflat_head_t))
/* the payload size in element */
#define SIZEOF_PAYLOAD(pmmflat, phead) ((mx_usize_t)mx_dlist_next(&phead->list) <= (mx_usize_t)phead ?   \
                                            ((mx_usize_t)END_ADDR(pmmflat) - (mx_usize_t)phead->payload) : ((mx_usize_t)mx_dlist_next(&phead->list) - (mx_usize_t)phead->payload))
/* the minimum size of element */
#define SIZEOF_MIN (SIZEOF_HEAD + MX_CPU_BITWIDE_BYTE)
/* the element end address */
#define ELE_END_ADDR(pmmflat, phead) ((mx_usize_t)mx_dlist_next(&phead->list) <= (mx_usize_t)phead ?    \
                                            END_ADDR(pmmflat) : (mx_void_pt)mx_dlist_next(&phead->list))
/* check whether the specified head is used */
#define ISUSED(phead) ((phead)->flist.prev == mx_null && (phead)->flist.next == mx_null)
/* align size */
#define ALIGN_SIZE(size) ((size + MX_CPU_BITWIDE_MASK) & ~MX_CPU_BITWIDE_MASK)

/**
 * mmflat init
 * @param pmmflat: the descriptor of mmflat
 * @param start: the start address of memory region
 * @param size: the size of the memory region
 * @return: void
 */
void mx_mmflat_init (mx_mmflat_pt pmmflat, mx_void_pt start, mx_usize_t size)
{
    assert(pmmflat == mx_null);
    assert(start == mx_null);
    assert(size < SIZEOF_MIN);

    pmmflat->start = start;
    pmmflat->size = size;
    pmmflat->fsize = size - SIZEOF_HEAD;

    mx_mmflat_head_pt phead = (mx_mmflat_head_pt)start;
    mx_dlist_init(&phead->list);
    mx_dlist_init(&phead->flist);
    
    pmmflat->base = &phead->list;
    pmmflat->fbase = &phead->flist;
}

/**
 * allocate a memory space
 * @param pmmflat: the descriptor of mmflat
 * @param size: the size of memory space
 * @return: the start address of memory space
 * @note: if the specified size is not `cpu aligned`, this function will make it align automatically
 */
mx_void_pt mx_mmflat_alloc (mx_mmflat_pt pmmflat, mx_usize_t size)
{
    mx_void_pt ret = mx_null;
    if (pmmflat != mx_null && size <= pmmflat->fsize && size != 0)  /* needn't checkout the `pmmflat->fbase != null` because of `size <= pmmflat->fsize` */
    {
        mx_dlist_foreach(pmmflat->fbase, dl, {
            mx_mmflat_head_pt phead = container(dl, mx_mmflat_head_t, flist);
            mx_usize_t pdsize = SIZEOF_PAYLOAD(pmmflat, phead);
            /* find a large enough element */
            if (size <= pdsize)
            {
                mx_usize_t align_size = ALIGN_SIZE(size);
                if ((pdsize - align_size) < SIZEOF_MIN) /* just the right size */   /* here can be changed to `(pdsize - align_size) < (SIZEOF_HEAD + 1)` if you want save the last element of memory region */
                {
                    /* remove from free chain */
                    if (pmmflat->fbase == dl)
                    {
                        pmmflat->fbase = mx_dlist_next(dl) == dl ? mx_null : mx_dlist_next(dl);
                    }
                    mx_dlist_remove(dl);
                    /* mark the element is used */
                    dl->next = mx_null;
                    dl->prev = mx_null;
                    /* update the mmflat */
                    pmmflat->fsize -= pdsize;
                }
                else                                    /* create a new element */
                {
                    /* new element */
                    mx_mmflat_head_pt newhead = (mx_mmflat_head_pt)((mx_usize_t)phead->payload + align_size);
                    mx_dlist_insert_after(dl, &newhead->flist);
                    mx_dlist_insert_after(&phead->list, &newhead->list);
                    /* update allocated phead and mark the element is used */
                    mx_dlist_remove(dl);
                    dl->next = mx_null;
                    dl->prev = mx_null;
                    /* update the mmflat */
                    pmmflat->fsize -= align_size + SIZEOF_HEAD;
                    if (pmmflat->fbase == dl)
                    {
                        pmmflat->fbase = &newhead->flist;
                    }
                }
                ret = (mx_void_pt)phead->payload;
                break;
            }
        });
    }
    return ret;
}

/**
 * free the specified memory space by address
 * @param pmmflat: the descriptor of mmflat
 * @param addr: the address of memory space
 * @return: void
 */
void mx_mmflat_free (mx_mmflat_pt pmmflat, mx_void_pt addr)
{
    assert(pmmflat == mx_null);

    mx_dlist_foreach(pmmflat->base, dl, {
        mx_mmflat_head_pt phead = container(dl, mx_mmflat_head_t, list);
        /* find the node contains the specify `addr` */
        if (addr >= (mx_void_pt)phead->payload && addr < ELE_END_ADDR(pmmflat, phead) && ISUSED(phead))
        {
            mx_mmflat_head_pt nearhead;

            /* free this element */
            if (pmmflat->fbase == mx_null)
            {
                mx_dlist_init(&phead->flist);       // mark to noused
            }
            else
            {
                mx_dlist_insert_before(pmmflat->fbase, &phead->flist);
            }
            pmmflat->fbase = &phead->flist;
            pmmflat->fsize += SIZEOF_PAYLOAD(pmmflat, phead);

            /* combine the right element if it is noused */
            if (mx_dlist_next(dl) > dl && !ISUSED(nearhead = container(mx_dlist_next(dl), mx_mmflat_head_t, list)))
            {
                pmmflat->fsize += SIZEOF_HEAD;
                mx_dlist_remove(&nearhead->list);
                mx_dlist_remove(&nearhead->flist);
            }
            /* combin the left element if it is noused */
            if (mx_dlist_prev(dl) < dl && !ISUSED(nearhead = container(mx_dlist_prev(dl), mx_mmflat_head_t, list)))
            {
                pmmflat->fbase = &nearhead->flist;
                pmmflat->fsize += SIZEOF_HEAD;
                mx_dlist_remove(&phead->list);
                mx_dlist_remove(&phead->flist);
            }
            break;
        }
    });
}

/**
 * get the total memory of mmflat
 * @param mmflat: the descriptor of mmflat
 * @return: total size of memory
 */
mx_usize_t mx_mmflat_total (mx_mmflat_pt pmmflat)
{
    return pmmflat->size;
}

/**
 * get the available memory of mmflat
 * @param mmflat: the descriptor of mmflat
 * @return: free size
 */
mx_usize_t mx_mmflat_available (mx_mmflat_pt pmmflat)
{
    return pmmflat->fsize;
}

/**
 * get the start address of memory
 * @param pmmflat: the descriptor of mmflat
 * @return: the start address of memeory
 */
mx_void_pt mx_mmflat_startaddr (mx_mmflat_pt pmmflat)
{
    return pmmflat->start;
}
